/**
 * This software is licensed under the MIT license. 
 */
package de.kune.sessionxs;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.kune.sessionxs.failurehandler.InvalidatingSessionMatchingFailureHandler;
import de.kune.sessionxs.failurehandler.SessionMatchingFailureHandler;
import de.kune.sessionxs.matcher.ClientIPAddressMatcher;
import de.kune.sessionxs.matcher.SessionMatcher;
import de.kune.sessionxs.matcher.UserAgentHeaderMatcher;
import de.kune.sessionxs.matcher.context.SessionMatcherContext;

/**
 * This class initializes and invokes session matchers and handles any matching
 * failure. The session matcher filter can be used in the web.xml file like
 * this:
 * 
 * <pre>
 * 	&lt;filter>
 * 	&lt;filter-name>SessionMatcherFilter&lt;/filter-name>
 * 	&lt;filter-class>de.kune.sessionxs.SessionMatcherFilter&lt;/filter-class>
 * 	&lt;init-param>
 * 		&lt;param-name>session-matchers&lt;/param-name>
 * 		&lt;param-value>de.kune.sessionxs.matcher.ClientIPAddressMatcher&lt;/param-value>
 * 	&lt;/init-param>
 * 	&lt;init-param>
 * 		&lt;param-name>failure-handler&lt;/param-name>
 * 		&lt;param-value>de.kune.sessionxs.failurehandler.InvalidatingSessionMatchingFailureHandler&lt;/param-value>
 * 	&lt;/init-param>
 * &lt;/filter>
 * 
 * ...
 * 
 * &lt;filter-mapping>
 * 	&lt;filter-name>SessionMatcherFilter&lt;/filter-name>
 * 	&lt;url-pattern>/*&lt;/url-pattern>
 * &lt;/filter-mapping>
 * 
 * </pre>
 * 
 * The init-parameter <code>session-matchers</code> specifies a list of
 * {@link SessionMatcher} classes that will be used to check if a request may
 * access a session. The init-parameter <code>failure-handler</code> specifies
 * the {@link SessionMatchingFailureHandler} class that handles any matching
 * failure.
 * 
 * @author Alexander Kune
 * 
 */
public class SessionMatcherFilter implements Filter {

	private static final Logger LOGGER = LoggerFactory
			.getLogger(SessionMatcherFilter.class);

	private String sessionIdentifierMapKey = this.getClass().getName()
			+ ".sessionIdentifierMap";

	private List<SessionMatcher> sessionMatchers = new LinkedList<SessionMatcher>();

	private SessionMatchingFailureHandler sessionMatchingFailureHandler;

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {

		String sessionMatchersListString = filterConfig
				.getInitParameter("session-matchers");
		if (sessionMatchersListString == null) {
			sessionMatchers.add(new ClientIPAddressMatcher());
			sessionMatchers.add(new UserAgentHeaderMatcher());
		} else {
			try {
				initSessionMatchers(sessionMatchersListString);
			} catch (Exception e) {
				throw new ServletException(
						"Could not initialize session matchers ["
								+ sessionMatchersListString + "].", e);
			}
		}

		String failureHandlerString = filterConfig
				.getInitParameter("failure-handler");
		if (failureHandlerString == null) {
			sessionMatchingFailureHandler = new InvalidatingSessionMatchingFailureHandler();
		} else {
			try {
				initFailureHandler(failureHandlerString);
			} catch (Exception e) {
				throw new ServletException(
						"Could not initialize failure handler ["
								+ failureHandlerString + "].", e);
			}
		}
	}

	private void initFailureHandler(String failureHandlerString)
			throws InstantiationException, IllegalAccessException,
			ClassNotFoundException {
		sessionMatchingFailureHandler = (SessionMatchingFailureHandler) Class
				.forName(failureHandlerString.trim()).newInstance();

	}

	private void initSessionMatchers(String sessionMatchersListString)
			throws InstantiationException, IllegalAccessException,
			ClassNotFoundException {
		for (String sessionMatcherString : sessionMatchersListString
				.split("[,\\s]")) {
			sessionMatchers.add((SessionMatcher) Class.forName(
					sessionMatcherString.trim()).newInstance());
		}
	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		if (request instanceof HttpServletRequest
				&& response instanceof HttpServletResponse) {
			HttpSession session = ((HttpServletRequest) request)
					.getSession(false);
			if (session != null) {
				@SuppressWarnings("unchecked")
				Map<String, SessionMatcherContext> sessionIdentifierMap = (Map<String, SessionMatcherContext>) session
						.getAttribute(this.sessionIdentifierMapKey);
				if (sessionIdentifierMap == null) {
					sessionIdentifierMap = new HashMap<String, SessionMatcherContext>();
					initSessionMatchers((HttpServletRequest) request,
							sessionIdentifierMap);
					session.setAttribute(this.sessionIdentifierMapKey,
							sessionIdentifierMap);
				} else {
					for (SessionMatcher sessionMatcher : sessionMatchers) {
						if (!sessionMatcher.matches(sessionIdentifierMap
								.get(sessionMatcher.getClass().getName()),
								(HttpServletRequest) request)) {
							if (LOGGER.isInfoEnabled()) {
								LOGGER.info("Session mismatch reported by ["
										+ sessionMatcher
										+ "], invoking handler ["
										+ sessionMatchingFailureHandler + "].");
							}
							if (LOGGER.isDebugEnabled()) {
								LOGGER.debug(

								"Session matcher context is ["
										+ sessionIdentifierMap
												.get(sessionMatcher.getClass()
														.getName()) + "].");
								LOGGER.debug("Request was [" + request + "].");
							}
							sessionMatchingFailureHandler
									.handleSessionMatchingFailure(chain,
											(HttpServletRequest) request,
											(HttpServletResponse) response);
							return;
						}
					}
				}
			}
		}
		chain.doFilter(request, response);
	}

	private void initSessionMatchers(HttpServletRequest request,
			Map<String, SessionMatcherContext> sessionIdentifierMap) {
		for (SessionMatcher sessionMatcher : sessionMatchers) {
			sessionIdentifierMap.put(sessionMatcher.getClass().getName(),
					sessionMatcher.init(request));
		}
	}

	@Override
	public void destroy() {
		sessionMatchers.clear();
		sessionMatchers = null;
		sessionMatchingFailureHandler = null;
	}

}
